import * as THREE from 'three';
import { EntityManager } from './EntityManager';
import { Passes } from './Passes';


const SCALE_1_ = new THREE.Vector3( 1, 1, 1 );

let IDS_ = 0;

export class Entity {
    id_: number;
    name_: string;
    components_: { [ key: string ]: Component; };
    attributes_: {};
    transform_: THREE.Matrix4;
    worldTransform_: THREE.Matrix4;
    position_: THREE.Vector3;
    rotation_: THREE.Quaternion;
    handlers_: {};
    parent_: Entity;
    dead_: boolean;
    active_: boolean;
    childrenActive_: Entity[];
    children_: Entity[];
    constructor ( name: string ) {
        IDS_ += 1;

        this.id_ = IDS_;
        this.name_ = name ? name : this.GenerateName_();
        this.components_ = {};
        this.attributes_ = {};

        this.transform_ = new THREE.Matrix4();
        this.transform_.identity();
        this.worldTransform_ = new THREE.Matrix4();
        this.worldTransform_.identity();

        this.position_ = new THREE.Vector3();
        this.rotation_ = new THREE.Quaternion();

        this.handlers_ = {};

        this.dead_ = false;
        this.active_ = true;

        this.childrenActive_ = [];
        this.children_ = [];

    }

    Destroy_ () {
        for ( let c of this.children_ ) {
            c.Destroy_();
        }
        for ( let k in this.components_ ) {
            this.components_[ k ].Destroy();
        }
        this.childrenActive_ = [];
        this.children_ = [];
        this.components_ = {};
        this.parent_ = null;
        this.handlers_ = {};
        this.Manager.Remove( this.name_ );
    }

    GenerateName_ () {
        return '__name__' + this.id_;
    }

    RegisterHandler_ ( n, h ) {
        if ( !( n in this.handlers_ ) ) {
            this.handlers_[ n ] = [];
        }
        this.handlers_[ n ].push( h );
    }

    UnregisterHandler_ ( name: string, handle ) {
        this.handlers_[ name ] = this.handlers_[ name ].filter( c => c != handle );
    }

    AddChild_ ( e ) {
        this.children_.push( e );
        this.RefreshActiveChildren_();
    }

    RemoveChild_ ( e ) {
        this.children_ = this.children_.filter( c => c != e );
        this.RefreshActiveChildren_();
    }

    SetParent ( p: Entity ) {
        if ( this.parent_ ) {
            this.parent_.RemoveChild_( this );
        }

        this.parent_ = p;

        if ( this.parent_ ) {
            this.parent_.AddChild_( this );
        }
    }

    get Name () {
        return this.name_;
    }

    get ID () {
        return this.id_;
    }

    get Manager () {
        return new EntityManager();
    }

    get Parent () {
        return this.parent_;
    }

    get Attributes () {
        return this.attributes_;
    }

    get Children () {
        return [ ...this.children_ ];
    }

    get IsDead () {
        return this.dead_;
    }

    get IsActive () {
        return this.active_;
    }

    RefreshActiveChildren_ () {
        this.childrenActive_ = this.children_.filter( c => c.IsActive );
    }

    SetActive ( active ) {
        this.active_ = active;
        if ( this.parent_ ) {
            this.parent_.RefreshActiveChildren_();
        }
    }

    SetDead () {
        this.dead_ = true;
    }

    AddComponent ( c: Component ) {
        c.SetParent( this );
        this.components_[ c.NAME ] = c;

        c.InitComponent();
    }

    Init ( parent?: Entity ) {
        this.Manager.Add( this, parent );
        this.InitEntity_();
    }

    InitEntity_ () {
        for ( let k in this.components_ ) {
            this.components_[ k ].InitEntity();
        }
        this.SetActive( this.active_ );
    }

    GetComponent ( name: string ) {
        return this.components_[ name ];
    }

    FindEntity ( name: string ) {
        return this.Manager.Get( name );
    }

    FindChild ( name: string, recursive: boolean ) {
        let result: Entity | null = null;

        for ( let i = 0, n = this.children_.length; i < n; ++i ) {
            if ( this.children_[ i ].Name == name ) {
                result = this.children_[ i ];
                break;
            }

            if ( recursive ) {
                result = this.children_[ i ].FindChild( name, recursive );
                if ( result ) {
                    break;
                }
            }
        }
        return result;
    }

    Broadcast ( msg ) {
        if ( this.IsDead ) {
            return;
        }
        if ( !( msg.topic in this.handlers_ ) ) {
            return;
        }

        for ( let curHandler of this.handlers_[ msg.topic ] ) {
            curHandler( msg );
        }
    }

    SetPosition ( p: THREE.Vector3Like ) {
        this.position_.copy( p );
        this.transform_.compose( this.position_, this.rotation_, SCALE_1_ );
        this.Broadcast( {
            topic: 'update.position',
            value: this.position_,
        } );
    }

    SetQuaternion ( r: THREE.QuaternionLike ) {
        this.rotation_.copy( r );
        this.transform_.compose( this.position_, this.rotation_, SCALE_1_ );
        this.Broadcast( {
            topic: 'update.rotation',
            value: this.rotation_,
        } );
    }

    get Transform () {
        return this.transform_;
    }

    get WorldTransform () {
        const m = this.worldTransform_.copy( this.transform_ );
        if ( this.parent_ ) {
            m.multiply( this.parent_.Transform );
        }
        return m;
    }

    GetWorldPosition ( target ) {
        target.setFromMatrixPosition( this.WorldTransform );
        return target;
    }

    get Position () {
        return this.position_;
    }

    get Quaternion () {
        return this.rotation_;
    }

    get Forward () {
        const forward = new THREE.Vector3( 0, 0, -1 );
        forward.applyQuaternion( this.rotation_ );
        return forward;
    }

    get Left () {
        const forward = new THREE.Vector3( -1, 0, 0 );
        forward.applyQuaternion( this.rotation_ );
        return forward;
    }

    get Up () {
        const forward = new THREE.Vector3( 0, 1, 0 );
        forward.applyQuaternion( this.rotation_ );
        return forward;
    }

    UpdateComponents_ ( elapsedTime: number, pass ) {
        for ( let k in this.components_ ) {
            const c = this.components_[ k ];
            if ( c.Pass == pass ) {
                c.Update( elapsedTime );
            }
        }
    }

    UpdateChildren_ ( elapsedTime: number, pass ) {
        const dead: Entity[] = [];
        const alive: Entity[] = [];
        for ( let i = 0; i < this.childrenActive_.length; ++i ) {
            const e = this.childrenActive_[ i ];

            e.Update( elapsedTime, pass );

            if ( e.IsDead ) {
                dead.push( e );
            } else {
                alive.push( e );
            }
        }

        let hasDead = false;
        for ( let i = 0; i < dead.length; ++i ) {
            const e = dead[ i ];

            e.Destroy_();
            hasDead = true;
        }

        if ( hasDead ) {
            this.children_ = this.children_.filter( c => !c.IsDead );
            this.RefreshActiveChildren_();
        }
    }

    Update ( elapsedTime: number, pass ) {
        this.UpdateComponents_( elapsedTime, pass );
        this.UpdateChildren_( elapsedTime, pass );
    }
};


export class Component {
    parent_: Entity;
    pass_: number;
    get NAME () {
        console.error( 'Unnamed Component: ' + this.constructor.name );
        return '__UNNAMED__';
    }

    constructor () {
        this.pass_ = Passes.DEFAULT;
    }

    Destroy () { }
    InitComponent () { }
    InitEntity () { }
    Update ( elapsedTime: number ) { }

    SetParent ( parent: Entity ) {
        this.parent_ = parent;
    }

    SetPass ( pass ) {
        this.pass_ = pass;
    }

    get Pass () {
        return this.pass_;
    }

    GetComponent ( name: string ) {
        return this.parent_ && this.parent_.GetComponent( name );
    }

    get Manager () {
        return this.parent_.Manager;
    }

    get Parent () {
        return this.parent_;
    }

    FindEntity ( name ) {
        return this.Manager.Get( name );
    }

    Broadcast ( m ) {
        this.parent_ && this.parent_.Broadcast( m );
    }

    RegisterHandler_ ( name, cb ) {
        this.parent_ && this.parent_.RegisterHandler_( name, cb );
    }
};